package com.example.webdriveddd.application;

import com.example.webdriveddd.domain.FileInfo;
import com.example.webdriveddd.domain.FileTag;
import com.example.webdriveddd.domain.User;
import com.example.webdriveddd.dto.*;
import com.example.webdriveddd.factory.FileInfoFactory;
import com.example.webdriveddd.repository.FileRepository;
import com.example.webdriveddd.repository.TagRepository;
import com.example.webdriveddd.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class Application {
    @Autowired
    private FileRepository fileRepository;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private FileInfoFactory fileInfoFactory;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private TagRepository tagRepository;

    @Transactional
    public FolderDTO getFolderByPath(String username,String path) throws AppException {
        Path p=Path.of(path);
        User user=userRepository.findByUsername(username).orElseThrow(()->{
            return new NotFoundException("user with name "+username+" not found",null);
        });
        FileInfo fileInfo= fileRepository.findRootFile(user.getId()).orElseThrow(()->{
            return new NotFoundException("root file of user with id "+user.getId()+" not found",null);
        });
        if (p.getNameCount()==0){
            return fileInfo.toFolderDTO();
        }
        for (int i=0;i<p.getNameCount();i++){
            String name=p.getName(i).toString();
            fileInfo=fileInfo.subFiles().stream().filter(file->{
                return file.getFileName().equals(name)&&file.isDir();
            }).findFirst().orElseThrow(()->{
                return new NotFoundException("sub file of user with id "+user.getId()+" not found",null);
            });
        }
        return fileInfo.toFolderDTO();
    }
    @Transactional
    public FolderDTO getFolder(String username,int folderId) throws NotFoundException {
        User user=userRepository.findByUsername(username).orElseThrow(()->{
            return new NotFoundException("user with name "+username+" not found",null);
        });

        FileInfo folder=null;
        if (folderId == -1) {
            folder=fileRepository.findRootFile(user.getId()).orElseThrow(()->{
                return new NotFoundException("root file of user with id "+user.getId()+" not found",null);
            });
        }else{
            folder=fileRepository.findById(folderId).orElseThrow(()->{
                return new NotFoundException("file with id "+folderId+" not found",null);
            });
        }

        return folder.toFolderDTO();
    }

    @Transactional(rollbackFor = AppException.class)
    public UserDTO register(String username, String password) throws IllegalArgException {
        password=passwordEncoder.encode(password);
        Optional<User> optionalUser=userRepository.findByUsername(username);
        if (optionalUser.isPresent()) {
            throw new IllegalArgException("username "+username+" already exists",null);
        }
        User user = userRepository.save(User.newUser(username, password));
        try {
            FileInfo rootFile=fileInfoFactory.rootFile(user.getId());
            fileRepository.save(rootFile);
            return user.toDTO();
        }catch (Exception ignoreException){
            ignoreException.printStackTrace();
        }
        return null;
    }

    @Transactional
    public Set<FileInfoDTO> getSubFiles(String username,int fileId) throws AppException {
        FileInfo parentFile=null;
        if (fileId==-1){
            parentFile=fileRepository.findRootFile(username).orElseThrow(()->{
                return new NotFoundException("root file not found",null);
            });
        }else{
            parentFile=fileRepository.findById(fileId).orElseThrow(()->{
                return new NotFoundException("file with id "+fileId+" not found",null);
            });
        }
        if (parentFile.isFile()){
            throw new IllegalArgException("file cannot list files",null);
        }
        return parentFile.subFiles().stream().map(FileInfo::toDTO).collect(Collectors.toSet());
    }

    @Transactional(rollbackFor = AppException.class)
    public void move(int sourceId,int targetDirId) throws AppException {
        FileInfo sourceFile=fileRepository.findById(sourceId).orElseThrow(()->{
            return new NotFoundException("source file with id "+sourceId+" not found",null);
        });
        FileInfo targetDir=fileRepository.findById(targetDirId).orElseThrow(()->{
            return new NotFoundException("targetDir with id "+targetDirId+" not found",null);
        });
        if (targetDir.isFile()){
            throw new IllegalArgException("targetDir must be a dir",null);
        }
        if (targetDir.subFiles().stream().anyMatch(file->{
            return file.fileInfoEquals(sourceFile);
        })){
            throw new IllegalArgException("file already exists",null);
        }
        sourceFile.setParentFile(targetDir);
        targetDir.subFiles().add(sourceFile);
        fileRepository.save(sourceFile);
        fileRepository.save(targetDir);
    }

    @Transactional
    public Set<FileTagDTO> getFileTags(int fileId) throws AppException {
        FileInfo fileInfo=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("source file with id "+fileId+" not found",null);
        });
        return fileInfo.tags().stream().map(FileTag::toDTO).collect(Collectors.toSet());
    }

    @Transactional
    public Set<FileInfoDTO> search(String username,String keyword) throws AppException {
        User user=userRepository.findByUsername(username).orElseThrow(()->{
            return new NotFoundException("user with name "+username+" not found",null);
        });
        Set<FileInfo> tagResultSet=fileRepository.searchByUserIdAndTagContains(user.getId(),keyword);
        Set<FileInfo> nameResultSet=fileRepository.searchByUserIdAndFileNameContains(user.getId(),keyword);
        return Stream.of(tagResultSet,nameResultSet).flatMap(Set::stream).distinct().map(FileInfo::toDTO).collect(Collectors.toSet());
    }

    @Transactional(rollbackFor = AppException.class)
    public FileInfoDTO newFile(FileInfoDTO fileInfoDTO) throws AppException {
        FileInfo parentFile=fileRepository.findById(fileInfoDTO.getParentId()).orElseThrow(()->{
            return new NotFoundException("parent file with id "+fileInfoDTO.getParentId()+" not found",null);
        });
        if (!parentFile.isDir()){
            throw new IllegalArgException("parent file must be a dir",null);
        }
        FileInfo fileInfo=fileInfoFactory.fromDTO(fileInfoDTO);
        if (parentFile.subFiles().stream().anyMatch(file->{
            return file.fileInfoEquals(fileInfo);
        })){
            throw new IllegalArgException("file already exists",null);
        }
        return fileRepository.save(fileInfo).toDTO();
    }

    @Transactional(rollbackFor = AppException.class)
    public void delete(int fileId){
        fileRepository.deleteById(fileId);
    }

    @Transactional
    public FileInfoDTO shareFile(int fileId,String password) throws AppException {
        FileInfo fileInfo=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("file with id "+fileId+" not found",null);
        });
        if (fileInfo.isDir()){
            throw new IllegalArgException("share dir do not support",null);
        }
        fileInfo.share(password);
        return fileRepository.save(fileInfo).toDTO();
    }

    @Transactional
    public void cancelFileShare(int fileId) throws AppException {
        FileInfo fileInfo=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("file with id "+fileId+" not found",null);
        });
        fileInfo.cancelShare();
        fileRepository.save(fileInfo);
    }

    @Transactional
    public void putTagToFile(int fileId,String tag) throws AppException {
        FileInfo fileInfo=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("file with id "+fileId+" not found",null);
        });
        FileTag fileTag=tagRepository.findByUserIdAndTag(fileInfo.getUser().getId(),tag)
                .orElseGet(() -> tagRepository.save(new FileTag(tag, fileInfo.getUser())));

        if (fileInfo.tags().stream().anyMatch((t)->{
            return t.tagEquals(fileTag);
        })){
            throw new IllegalArgException("tag with name "+tag+" already exists",null);
        }

        fileInfo.tags().add(fileTag);
        tagRepository.save(fileTag);
        fileRepository.save(fileInfo);
    }

    @Transactional
    public void removeTagFromFile(int fileId,String tag) throws AppException {
        FileInfo fileInfo=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("file with id "+fileId+" not found",null);
        });
        FileTag fileTag = tagRepository.findByUserIdAndTag(fileInfo.getUser().getId(), tag)
                .orElseThrow(() -> {
                    return new NotFoundException("tag with name " + tag + " not found", null);
                });

        fileInfo.tags().remove(fileTag);

        tagRepository.save(fileTag);
        fileRepository.save(fileInfo);
    }

    @Transactional
    public Set<FileInfoDTO> findFilesByTag(String username,String tag) throws AppException {
        return fileRepository.findFileInfosByUsernameAndTag(username,tag).stream().map(FileInfo::toDTO).collect(Collectors.toSet());
    }

    @Transactional
    public Set<FileTagDTO> findTagsByUsername(String username){
        return tagRepository.findFileTagsByUser_Username(username).stream().map(FileTag::toDTO).collect(Collectors.toSet());
    }

    @Transactional
    public FileInfoDTO saveShare(String username,int fileId) throws AppException {
        FileInfo needSaveFile=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("file with id "+fileId+" not found",null);
        });

        FileInfo parentFile=null;
        FileInfo rootFile=fileRepository.findRootFile(username).orElseThrow(()->{
            return new NotFoundException("root file with id "+fileId+" not found",null);
        });

        User user=userRepository.findByUsername(username).orElseThrow(()->{
            return new NotFoundException("user not found",null);
        });
        List<FileInfo> matchedFiles=rootFile.subFiles().stream().filter(subFile->{
            return subFile.getFileName().equals("保存的文件")&&subFile.isDir();
        }).toList();
        if (matchedFiles.size()!=1){
            FileInfo fileInfo=new FileInfo("保存的文件","dir",0,rootFile,user);
            rootFile.subFiles().add(fileInfo);
            parentFile=fileInfo;
        }else {
            parentFile = matchedFiles.get(0);
        }

        FileInfo needSave=needSaveFile.copyFileInfo(parentFile,user);

        if(parentFile.subFiles().stream().anyMatch((file)->{
            return file.fileInfoEquals(needSave);
        })){
            throw new IllegalArgException("file already exists",null);
        }

        FileInfo savedFile=fileRepository.save(needSave);
        fileRepository.save(parentFile);
        fileRepository.save(rootFile);
        return savedFile.toDTO();
    }

    @Transactional
    public FileInfoDTO saveShare(String username,String password,int fileId,int targetDirId) throws NotFoundException, IllegalArgException {
        FileInfo needSaveFile=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("file with id "+fileId+" not found",null);
        });

        boolean verifyResult=false;

        try {
            verifyResult=needSaveFile.verifySharePassword(password);
        }catch (Exception e){
            throw new IllegalArgException("file with id "+fileId+" is not sharing",e);
        }

        if (!verifyResult){
            throw new IllegalArgException("password of share incorrect",null);
        }

        FileInfo parentFile=fileRepository.findById(targetDirId).orElseThrow(()->{
            return new NotFoundException("file with id "+targetDirId+" not found",null);
        });
        User user=userRepository.findByUsername(username).orElseThrow(()->{
            return new NotFoundException("user with username "+username+" not found",null);
        });
        if (parentFile.isFile()){
            throw new IllegalArgException("target file must be a dir",null);
        }

        FileInfo needSave=needSaveFile.copyFileInfo(parentFile,user);

        if(parentFile.subFiles().stream().anyMatch((file)->{
            return file.fileInfoEquals(needSave);
        })){
            throw new IllegalArgException("file already exists",null);
        }

        FileInfo savedFile=fileRepository.save(needSave);
        fileRepository.save(parentFile);
        return savedFile.toDTO();
    }

    @Transactional
    public ShareInfoDTO accessFile(int fileId) throws NotFoundException {
        FileInfo fileInfo=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("file with id "+fileId+" not found",null);
        });
        if (!fileInfo.isShare()){
            throw new NotFoundException("file with id "+fileId+" is not share",null);
        }
        ShareInfoDTO shareInfoDTO=new ShareInfoDTO();
        shareInfoDTO.setFileId(fileInfo.getId());
        shareInfoDTO.setFileName(fileInfo.getFileName());
        shareInfoDTO.setUsername(fileInfo.getUser().getUsername());
        shareInfoDTO.setFileSize(fileInfo.getFileSize());
        return shareInfoDTO;
    }

    @Transactional
    public FileInfoDTO cancelShare(int fileId) throws AppException {
        FileInfo fileInfo=fileRepository.findById(fileId).orElseThrow(()->{
            return new NotFoundException("file with id "+fileId+" not found",null);
        });
        fileInfo.cancelShare();
        return fileRepository.save(fileInfo).toDTO();
    }
}
